﻿// <copyright file="CompressedIndexHeader.cs" company="Sinclair Community College">
// Copyright 2010 Sinclair Community College
// </copyright>
// <originalAuthor>Patrick S. Seymour (patrick.seymour@gmail.com)</originalAuthor>

namespace SinclairCC.AppV
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using ICSharpCode.SharpZipLib;

    /// <summary>
    /// This class represents the Compressed Index Header section in an App-V SFT/DSFT file.
    /// </summary>
    /// <remarks>The Compressed Index header carries two separate data payloads. The CIDX
    /// section’s primary purpose is to store the ACLs and Access Control Entries (ACEs) in
    /// the Security Descriptor (SECD) subsection. The secondary purpose is to describe the
    /// internal directory structure — or directory map — for the files and directories stored
    /// within the SFT file.</remarks>
    public class CompressedIndexHeader : HeaderMetadataSection
    {
        /// <summary>
        /// In App-V packages prior to version 4.5, there are eight reserved bytes in the CIDX
        /// header, immediately following the common section header.
        /// In App-V packages for version 4.5 onward, there are only three reserved bytes.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "This field is marked as reserved in the App-V File Format Specification.")]
        private byte[] reserved;

        /// <summary>
        /// This field is reserved and is initialized to all zeros. Currently the CIDX section
        /// is always compressed, so this field is not used.
        /// </summary>
        /// <remarks>This field only exists in packages for App-V 4.5 onward.</remarks>
        private int compressionType;

        /// <summary>
        /// Indicates whether the security descriptors, which are present in the package,
        /// are enforced (i.e., set by the App-V client) at package runtime.
        /// </summary>
        /// <remarks>This field only exists in packages for App-V 4.5 onward.</remarks>
        private bool securityDescriptorsEnforced;

        /// <summary>
        /// The total size of the security descriptor subsection in the CIDX section.
        /// </summary>
        /// <remarks>
        /// If compression is used for the package, size in this field indicates the
        /// compressed size, not the uncompressed one.
        /// </remarks>
        private int securityDescriptorSubsectionSize;

        /// <summary>
        /// The size of the directory map subsection that immediately follows the security
        /// descriptor subsection.
        /// </summary>
        /// <remarks>
        /// If compression is used for the package, size in this field indicates the
        /// compressed size, not the uncompressed one.
        /// </remarks>
        private int directoryMapSectionSize;

        /// <summary>
        /// A collection of security entries for this package.
        /// </summary>
        private List<SecdEntry> securityEntries;

        /// <summary>
        /// A collection of directory map entries for this package.
        /// </summary>
        private DirectoryMapEntryCollection directoryMapEntries;

        /// <summary>
        /// Indicates the type of compression used in the package.
        /// </summary>
        private CompressionMethod compressionUsed;

        /// <summary>
        /// Initializes a new instance of the CompressedIndexHeader class.
        /// </summary>
        /// <param name="fileReader">A binary file reader that points to the SFT file.</param>
        /// <param name="compressionInUse">The type of compression in use for this package.</param>
        public CompressedIndexHeader(System.IO.BinaryReader fileReader, CompressionMethod compressionInUse) : base(fileReader)
        {
            this.compressionUsed = compressionInUse;
        }

        /// <summary>
        /// Gets a value indicating whether security descriptors contained in the package are to
        /// be enforced by the App-V client.
        /// </summary>
        public bool SecurityDescriptorsEnforced
        {
            get
            {
                this.ReadSection();
                return this.securityDescriptorsEnforced;
            }
        }

        /// <summary>
        /// Gets the size of the security descriptor subsection in the CIDX header.
        /// </summary>
        public int SecurityDescriptorSubsectionSize
        {
            get
            {
                this.ReadSection();
                return this.securityDescriptorSubsectionSize;
            }
        }

        /// <summary>
        /// Gets the size of the directory map subsection in the CIDX header.
        /// </summary>
        public int DirectoryMapSectionSize
        {
            get
            {
                this.ReadSection();
                return this.directoryMapSectionSize;
            }
        }

        /// <summary>
        /// Gets a collection of security entries for this package.
        /// </summary>
        public List<SecdEntry> SecurityEntries
        {
            get
            {
                this.ReadSection();
                return this.securityEntries;
            }
        }

        /// <summary>
        /// Gets a collection of directory map entries for this package.
        /// </summary>
        public DirectoryMapEntryCollection DirectoryEntries
        {
            get
            {
                this.ReadSection();
                return this.directoryMapEntries;
            }
        }

        /// <summary>
        /// Gets the name of the package's root directory.
        /// </summary>
        public string RootDirectory
        {
            get
            {
                DirectoryMapEntry rootEntry = this.DirectoryEntries.FindRootEntry();
                return rootEntry.LongFileName;
            }
        }

        /// <summary>
        /// Finds the GUID of the directory map entry for the OSGUARD.CP file.
        /// </summary>
        /// <returns>Returns the GUID for the OSGUARD.CP file's directory map entry.</returns>
        public Guid FindOSGuardFileGuid()
        {
            DirectoryMapEntry rootEntry = this.directoryMapEntries.FindRootEntry();
            DirectoryMapEntryCollection rootChildren = this.directoryMapEntries.FindChildItems(rootEntry.ID);
            for (int i = 0; i < rootChildren.Count; i++)
            {
                if (string.Compare(rootChildren[i].LongFileName, "osguard.cp", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    return rootChildren[i].ID;
                }
            }

            return Guid.Empty;
        }
        
        /// <summary>
        /// Reads the actual data for this section from the file.
        /// </summary>
        protected override void ReadSection()
        {
            if (this.SectionHasBeenRead)
            {
                return;
            }

            // Seek to the start of the data area for this section.
            long currentPosition = FileReader.BaseStream.Position;
            FileReader.BaseStream.Seek(this.StartPositionInFile + CommonSectionHeader.HeaderSize, System.IO.SeekOrigin.Begin);

            // Read the properties from the file.
            this.securityEntries = new List<SecdEntry>();
            this.directoryMapEntries = new DirectoryMapEntryCollection();

            // These are the default values for these fields. We set them here in case the
            // file does not specify them.
            this.compressionType = 0;
            this.securityDescriptorsEnforced = true;

            // Since we don't know the App-V version the package was saved under at this point,
            // we will just read the file as if it were a 4.5-version file. After we know the
            // package version, these values can be reset if needed.
            this.compressionType = FileReader.ReadInt32();
            this.securityDescriptorsEnforced = FileReader.ReadByte() == 1;
            this.reserved = FileReader.ReadBytes(3);

            this.securityDescriptorSubsectionSize = FileReader.ReadInt32();
            this.directoryMapSectionSize = FileReader.ReadInt32();

            this.ReadSecurityDescriptorEntries(FileReader, this.compressionUsed);
            this.ReadDirectoryEntries(FileReader, this.compressionUsed);

            this.SectionHasBeenRead = true;
            FileReader.BaseStream.Seek(currentPosition, System.IO.SeekOrigin.Begin);
        }

        /// <summary>
        /// Reads the specified stream into a MemoryStream.
        /// </summary>
        /// <param name="inflatorStream">The stream to be read into the memory stream.</param>
        /// <returns>Returns a MemoryStream object containing the contents of the passed
        /// stream. The position within the stream is set to the beginning of the stream.
        /// </returns>
        private static System.IO.MemoryStream ReadStream(System.IO.Stream inflatorStream)
        {
            System.IO.MemoryStream memStream = new System.IO.MemoryStream();
            byte[] buffer = new byte[100];

            // Read the passed stream into the MemoryStream.
            long totalBytesRead = 0;
            int numBytesRead = inflatorStream.Read(buffer, 0, buffer.Length);
            while (numBytesRead > 0)
            {
                totalBytesRead += numBytesRead;
                memStream.Write(buffer, 0, numBytesRead);
                numBytesRead = inflatorStream.Read(buffer, 0, buffer.Length);
            }

            // Seek to the beginning of the stream.
            memStream.Seek(0, System.IO.SeekOrigin.Begin);

            return memStream;
        }

        /// <summary>
        /// Reads the specified number of bytes from a BinaryReader into a MemoryStream.
        /// </summary>
        /// <param name="fileReader">The BinaryReader object from which the bytes are to be read.</param>
        /// <param name="byteCount">The number of bytes to read from the BinaryReader.</param>
        /// <returns>
        /// Returns a MemoryStream object containing the bytes from the file. The position
        /// within the stream is set to the beginning of the stream.
        /// </returns>
        private static System.IO.MemoryStream GetBytesFromFile(System.IO.BinaryReader fileReader, int byteCount)
        {
            System.IO.MemoryStream compressedMemoryStream = new System.IO.MemoryStream();

            for (int i = 0; i < byteCount; i++)
            {
                compressedMemoryStream.WriteByte(fileReader.ReadByte());
            }

            // Seek to the beginning of the stream.
            compressedMemoryStream.Seek(0, System.IO.SeekOrigin.Begin);

            return compressedMemoryStream;
        }

        /// <summary>
        /// Reads the DirectoryEntry objects from the specified BinaryReader.
        /// </summary>
        /// <param name="reader">The BinaryReader that contains the DirectoryEntry objects.</param>
        /// <param name="numberOfBytes">The number of bytes to read from the BinaryReader.</param>
        private void ReadDirectoryEntries(System.IO.BinaryReader reader, long numberOfBytes)
        {
            this.directoryMapEntries.Clear();
            long directoryMapStart = reader.BaseStream.Position;
            long bytesRead = 0;
            while (bytesRead < numberOfBytes)
            {
                DirectoryMapEntry bme = new DirectoryMapEntry(reader);
                this.directoryMapEntries.Add(bme);
                bytesRead = reader.BaseStream.Position - directoryMapStart;
            }            
        }

        /// <summary>
        /// Reads the DirectoryEntry objects from the specified BinaryReader.
        /// </summary>
        /// <param name="fileReader">The BinaryReader that contains the DirectoryEntry objects.</param>
        /// <param name="compression">The type of compression used in the package.</param>
        private void ReadDirectoryEntries(System.IO.BinaryReader fileReader, CompressionMethod compression)
        {
            switch (compression)
            {
                case CompressionMethod.None:

                    this.ReadDirectoryEntries(fileReader, this.directoryMapSectionSize);
                    break;

                case CompressionMethod.bzip2:

                    System.IO.MemoryStream compressedMemoryStream = GetBytesFromFile(fileReader, this.directoryMapSectionSize);
                    ICSharpCode.SharpZipLib.BZip2.BZip2InputStream inStream = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(compressedMemoryStream);

                    System.IO.MemoryStream decompressedMemoryStream = ReadStream(inStream);
                    
                    inStream.Close();
                    inStream.Dispose();
                    compressedMemoryStream.Close();
                    compressedMemoryStream.Dispose();
                    
                    System.IO.BinaryReader reader = new System.IO.BinaryReader(decompressedMemoryStream);
                    this.ReadDirectoryEntries(reader, decompressedMemoryStream.Length);

                    break;

                // TODO: Need to find an SFT that does ZLib compression and compare it to SFT Explorer.
                // PerfectForm just has too much stuff in it to verify and keep your sanity.
                case CompressionMethod.ZLib:

                    System.IO.MemoryStream compressedMemoryStream2 = GetBytesFromFile(fileReader, this.directoryMapSectionSize);
                    ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream inStream2 = new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream(compressedMemoryStream2);

                    System.IO.MemoryStream decompressedMemoryStream2 = ReadStream(inStream2);
                    
                    inStream2.Close();
                    inStream2.Dispose();
                    compressedMemoryStream2.Close();
                    compressedMemoryStream2.Dispose();
                    
                    System.IO.BinaryReader reader2 = new System.IO.BinaryReader(decompressedMemoryStream2);
                    this.ReadDirectoryEntries(reader2, decompressedMemoryStream2.Length);

                    break;
            }
        }

        /// <summary>
        /// Reads the SecdEntry objects from the specified BinaryReader.
        /// </summary>
        /// <param name="reader">The BinaryReader that contains the SecdEntry objects.</param>
        /// <param name="numberOfBytes">The number of bytes to read from the BinaryReader.</param>
        private void ReadSecurityDescriptorEntries(System.IO.BinaryReader reader, long numberOfBytes)
        {
            this.securityEntries.Clear();
            long securityDescriptorSectionStart = reader.BaseStream.Position;
            long bytesRead = 0;
            while (bytesRead < numberOfBytes)
            {
                SecdEntry seh = new SecdEntry(reader);
                this.securityEntries.Add(seh);
                bytesRead = reader.BaseStream.Position - securityDescriptorSectionStart;
            }
        }

        /// <summary>
        /// Reads the SecdEntry objects from the specified BinaryReader.
        /// </summary>
        /// <param name="fileReader">The BinaryReader that contains the SecdEntry objects.</param>
        /// <param name="compression">The type of compression used in the package.</param>
        private void ReadSecurityDescriptorEntries(System.IO.BinaryReader fileReader, CompressionMethod compression)
        {
            switch (compression)
            {
                case CompressionMethod.None:
                    this.ReadSecurityDescriptorEntries(fileReader, this.securityDescriptorSubsectionSize);
                    break;

                case CompressionMethod.bzip2:

                    System.IO.MemoryStream compressedMemoryStream = GetBytesFromFile(fileReader, this.securityDescriptorSubsectionSize);
                    ICSharpCode.SharpZipLib.BZip2.BZip2InputStream inStream = new ICSharpCode.SharpZipLib.BZip2.BZip2InputStream(compressedMemoryStream);

                    System.IO.MemoryStream decompressedMemoryStream = ReadStream(inStream);

                    inStream.Close();
                    inStream.Dispose();
                    compressedMemoryStream.Close();
                    compressedMemoryStream.Dispose();

                    System.IO.BinaryReader reader = new System.IO.BinaryReader(decompressedMemoryStream);
                    this.ReadSecurityDescriptorEntries(reader, decompressedMemoryStream.Length);

                    break;

                // TODO: Need to find an SFT that does ZLib compression and compare it to SFT Explorer.
                // PerfectForm just has too much stuff in it to verify and keep your sanity.
                case CompressionMethod.ZLib:

                    System.IO.MemoryStream compressedMemoryStream2 = GetBytesFromFile(fileReader, this.securityDescriptorSubsectionSize);
                    ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream inStream2 = new ICSharpCode.SharpZipLib.Zip.Compression.Streams.InflaterInputStream(compressedMemoryStream2);

                    System.IO.MemoryStream decompressedMemoryStream2 = ReadStream(inStream2);

                    inStream2.Close();
                    inStream2.Dispose();
                    compressedMemoryStream2.Close();
                    compressedMemoryStream2.Dispose();

                    System.IO.BinaryReader reader2 = new System.IO.BinaryReader(decompressedMemoryStream2);
                    this.ReadSecurityDescriptorEntries(reader2, decompressedMemoryStream2.Length);

                    break;
            }
        }
    }
}
